feat: power ramp intervals in workout editor (W and % FTP) - #4809
Conversation
- Replace powerEnd with rampIsFtpFraction/rampPowerFromOriginal/rampPowerToOriginal
on trainrow so ramp metadata survives the expand-to-1s pass
- saveXML emits powerzonefrom/powerzoneto (FTP%) or powerfrom/powerto (W) compactly
- loadXML tags every expanded row with its ramp origin so they can be re-collapsed
- onGetTrainingProgram collapses consecutive ramp rows back into one compact entry
for the workout editor, adding powerrampunit ('W' or '% FTP') to the JSON
- onSaveTrainingProgram handles powerfrom/powerto and powerzonefrom/powerzoneto
- workout-editor-app.js: add 'Ramp Unit' combo (W / % FTP), dynamic label suffix,
select rendering, updated buildPayload and convertRow for round-trip fidelity
- Send FTP value in workouteditor_env so the chart can render % FTP ramps correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The spare-seconds distribution in the powerfrom/powerto and powerzonefrom/powerzoneto ramp-expansion loops used `i % spareSeconds == 0` to decide which 1-second segments get an extra second. This only fires floor(delta/spareSeconds) times instead of spareSeconds times, so a chunk of the leftover seconds was silently dropped (e.g. a 2:00 ramp collapsed to 1:42 after save+reload). The dead "i == delta" catch-up branch never executed either, since the loop condition is i < delta. Replaced it with an even (Bresenham-style) distribution based on cumulative counts, which guarantees the spare seconds sum up exactly across all expanded rows, and fixed the rampElapsed/ rampDuration bookkeeping to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On-device testing (WayDroid, x86_64 debug build)Built and installed a debug APK from this branch on a real Android (WayDroid) device and exercised the built-in Workout Editor end to end: creating a ramp interval, saving, reopening, and inspecting the saved XML. SetupBike device + Advanced mode expose the new fields as expected: Test 1 — Ramp in Watts (40 W → 90 W, 2:00)Set Saved as Pulled the saved file from the device — compact single row as promised, not hundreds of 1s segments: <?xml version="1.0" encoding="UTF-8"?>
<rows device="bike">
<row duration="00:02:00" resistance="20" cadence="80" powerfrom="40" powerto="90" forcespeed="0" looptimehr="10">
<textevent timeoffset="0" message="Interval 1"/>
</row>
</rows>Reloaded via "Select saved workout" → Load: editor correctly shows 1 interval, Bug found & fixed (see next comment for details): on first pass, the reloaded Test 2 — Ramp in % FTP (27% → 60%, 2:00)Same flow with
Saved as Saved XML, compact as expected: <?xml version="1.0" encoding="UTF-8"?>
<rows device="bike">
<row duration="00:02:00" resistance="20" cadence="80" powerzonefrom="0.2700" powerzoneto="0.6000" forcespeed="0" looptimehr="10">
<textevent timeoffset="0" message="Interval 1"/>
</row>
</rows>Reloaded: 1 interval, Summary
|
Bug fix: ramp duration lost on save/reload round-tripWhile testing, saving a 2:00 ramp interval and reloading it showed a shorter duration ( Root cause ( spare = (i % spareSeconds == 0 && i > 0) ? 1 : 0;This only adds an extra second on iterations where This exact pattern was pre-existing in the Fix: replaced the modulo check with an even (Bresenham-style) distribution using cumulative counts: int before = (int)((qint64)i * spareSeconds / delta);
int after = (int)((qint64)(i + 1) * spareSeconds / delta);
spare = after - before;This guarantees the Applied to both the Pushed as |
Ramp Unit never gated anything on save (only the Ramp From/Ramp To checkboxes decide whether the ramp is applied), so the extra enable/disable checkbox on it was just friction: users had to tick it before they could even see whether it was set to W or % FTP. Made it a `noToggle` field so it always renders as an active dropdown, defaulting to W. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
UX tweak: Ramp Unit no longer needs its own enable checkboxPer feedback: Changed it to always render as an active dropdown ( Pushed as |
Screenshots — Ramp Unit UX fix (
|
Chart.js's `stepped: true` was hardcoded for every dataset in the chart renderer (pre-existing, unrelated to this PR). That's correct for series like resistance/cadence, whose points already encode sharp transitions via duplicate x values at interval boundaries, but it forces a step interpolation between a ramp's two distinct-x points too, turning the intended diagonal (fromW at start, toW at end) into a flat segment followed by a vertical jump at the very end - which is what showed up as a stray vertical line in the preview. Added a per-series `stepped` flag (default true, unchanged for resistance/cadence/etc.), set to false for the power series (bike and rower) since it's the only one that can carry a ramp. Flat (non-ramp) power segments look identical whether stepped or not, so this doesn't affect the non-ramp case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chart preview slope — root cause + fix (
|
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
|
Mail from Anthony CHENE |














Summary
Adds native power ramp interval support to the built-in workout editor, allowing users to define intervals where power increases or decreases linearly from one value to another over the interval duration.
.zworamp support unchanged (<Warmup>,<Ramp>,<Cooldown>tags keep working as before)How it works
XML format
powerfrom="40" powerto="90"powerzonefrom="0.2700" powerzoneto="0.6000"At load time both formats are expanded into 1-second
trainrowsegments with linearly interpolated power (same mechanism used by Zwift workouts). Each expanded row carries the original endpoints (rampPowerFromOriginal/rampPowerToOriginal) so the backend can collapse them back into a single compact row when the editor requests the program.Data flow
Files changed
src/trainprogram.hrampIsFtpFraction,rampPowerFromOriginal,rampPowerToOriginaltotrainrowsrc/trainprogram.cppsaveXML: writes compact ramp attributes;loadXML: tags expanded rows with original endpointssrc/templateinfosenderbuilder.cpponGetTrainingProgram: collapses ramp sequences;onSaveTrainingProgram: parses both ramp formatssrc/inner_templates/workouteditor/workout-editor-app.jsselectfield type, 3 new FIELD_DEFS entries,buildPayloadramp override,convertRowramp detection, chart slope renderingTest plan
.zwofile with<Warmup>/<Ramp>/<Cooldown>→ still works, no regression🤖 Generated with Claude Code